﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Web.Http.OData.Builder;
using Microsoft.Data.Edm;

namespace Disco.Web.Api.Controllers.Dynamic
{
    public class DynamicControllerGenerator
    {
        private readonly IEnumerable<EntitySetConfiguration> _entitySets;

        private string _assemblyVersion;
        private string _currentPath;

        public DynamicControllerGenerator(IEnumerable<EntitySetConfiguration> entitySets)
        {
            _entitySets = entitySets;
        }

        public Assembly CreateAssembly(string controllerName)
        {
            if (String.IsNullOrEmpty(controllerName))
            {
                return null;
            }
            string outputDll = String.Concat(controllerName.Replace(" ", string.Empty), ".dll");
            _currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase).Replace("file:\\", String.Empty);
            string outputAssembly = Path.Combine(_currentPath, outputDll);

            Assembly dynamicAssembly = ResolveDynamicAssembly(outputAssembly);
            if (null != dynamicAssembly)
            {
                return dynamicAssembly;
            }

            IDictionary<string, string> compilerParams =
                 new Dictionary<string, string>() { { "CompilerVersion", "v4.0" } };
            CodeDomProvider codeProvider = CodeDomProvider.CreateProvider("CSharp", compilerParams);

            CompilerParameters parameters = new CompilerParameters();
            parameters.GenerateExecutable = false;
            parameters.OutputAssembly = outputAssembly;
            parameters.TempFiles = new TempFileCollection(_currentPath, false);
            parameters.ReferencedAssemblies.Add("System.Net.Http.dll");
            parameters.ReferencedAssemblies.Add("System.Net.Http.WebRequest.dll");
            parameters.ReferencedAssemblies.Add("System.Web.dll");
            parameters.ReferencedAssemblies.Add("mscorlib.dll");
            parameters.ReferencedAssemblies.Add(Path.Combine(_currentPath, "System.Web.Http.dll"));
            parameters.ReferencedAssemblies.Add(Path.Combine(_currentPath, "System.Web.Http.OData.dll"));
            parameters.ReferencedAssemblies.Add(Path.Combine(_currentPath, "Disco.Core.dll"));
            parameters.ReferencedAssemblies.Add(Path.Combine(_currentPath, "Disco.Ontology.dll"));
            parameters.ReferencedAssemblies.Add(Path.Combine(_currentPath, "Disco.Web.Api.dll"));

            // HINT: Might be better to generate assembly from Code DOM instead of source code!
            // TODO: As a Developer I want to refactor the code generation of the dynamic controller assembly so that the Code DOM approach is used instead of source code string concatenation.
            var sb = WriteDynamicAssemblySource(new StringBuilder());

            var assembly = CompileAssembly(outputDll, codeProvider, parameters, sb.ToString());
            return assembly;
        }

        private StringBuilder WriteDynamicAssemblySource(StringBuilder sb)
        {
            WriteDynamicModuleUsingStatementsSource(sb);
            WriteDynamicModuleAssemblyVersionSource(sb)
                .AppendLine("namespace Disco.Web.Api.Controllers.Dynamic")
                .AppendLine("{");

            WriteDynamicControllersSource(sb)
                .AppendLine("}");

            return sb;
        }

        private StringBuilder WriteDynamicModuleAssemblyVersionSource(StringBuilder sb)
        {
            return sb.AppendLine("")
                .AppendFormat("[assembly: AssemblyVersion(\"{0}\")]", _assemblyVersion)
                .AppendLine("")
                .AppendLine("");
        }

        private StringBuilder WriteDynamicModuleUsingStatementsSource(StringBuilder sb)
        {            
            return sb.AppendLine("using System;")
                .AppendLine("using System.Reflection;")
                .AppendLine("using System.Runtime.CompilerServices;")
                .AppendLine("using System.Runtime.InteropServices;")
                .AppendLine("using System.Collections.Generic;")
                .AppendLine("using System.Web.Http;")
                .AppendLine("using System.Web.Http.OData;")
                .AppendLine("using Disco.Ontology;")
                .AppendLine("using Disco.Web.Api.Controllers.Dynamic;")
                .AppendLine("");
        }

        private StringBuilder WriteDynamicControllersSource(StringBuilder sb)
        {
            foreach (var entitySet in _entitySets)
            {
                WriteDynamicControllerDeclarationSource(sb, entitySet);
            }

            return sb;
        }

        private StringBuilder WriteDynamicControllerDeclarationSource(StringBuilder sb, EntitySetConfiguration entitySet)
        {
            var schemaType = entitySet.ClrType;

            if (!schemaType.IsAbstract)
            {
                sb.AppendFormat("    public class {0}Controller : DynamicControllerBase<{1}>", entitySet.Name, schemaType.FullName)
                    .AppendLine("")
                    .AppendLine("    {");
                WriteDynamicControllerNavigationPropertyGetters(sb, entitySet)
                    .AppendLine("    }")
                    .AppendLine("");
            }

            return sb;
        }

        private StringBuilder WriteDynamicControllerNavigationPropertyGetters(StringBuilder sb, EntitySetConfiguration entitySet)
        {
            foreach (var binding in entitySet.Bindings)
            {
                WriteDynamicControllerNavigationPropertyGetterSource(sb, binding.NavigationProperty); 
            }

            return sb;
        }

        private StringBuilder WriteDynamicControllerNavigationPropertyGetterSource(StringBuilder sb, NavigationPropertyConfiguration navigationProperty)
        {
            var navigationType = navigationProperty.RelatedClrType.Name;
            var navigationName = navigationProperty.Name;

            var isMultiplicityMany = (EdmMultiplicity.Many == navigationProperty.Multiplicity);
            var returnType = String.Format(isMultiplicityMany ? "ICollection<{0}>" : "{0}", navigationType);
            var navigationMethod = isMultiplicityMany ? "LinkCollection" : "Link";

            sb.AppendFormat("        public {0} Get{1}([FromODataUri] long key)", returnType, navigationName)
                .AppendLine("")
                .AppendLine("        {")
                .AppendFormat("            return base.Get{0}<{1}>(key, entity => entity.{2});", navigationMethod, navigationType, navigationName)
                .AppendLine("")
                .AppendLine("        }");

            return sb;
        }

        private Assembly CompileAssembly(string outputDll, CodeDomProvider codeProvider, CompilerParameters parameters, string code)
        {
            CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, code);
            if (results.Errors.Count.Equals(0))
            {
                Debug.WriteLine("Build succeeded!", outputDll);

                // HINT: Unfortunately it seems previous loaded assemblies are served here instead of the new compiled one!
                // TODO: As a Developer I want the new compiled dynamic controller assembly to be loaded so that a potentially previously loaded one is discarded.
                // return results.CompiledAssembly;
                return Assembly.LoadFrom(results.PathToAssembly);
            }
            else
            {
                Debug.WriteLine("Build failed!", outputDll);

                foreach (CompilerError compError in results.Errors)
                {
                    Debug.WriteLine(String.Format("Line number {0}, Error Number: {1}, '{2}';",
                        compError.Line,
                        compError.ErrorNumber,
                        compError.ErrorText), outputDll);
                }
            }

            return null;
        }

        private Assembly ResolveDynamicAssembly(string assemblyName)
        {
            var ontologyAssembly = Assembly.LoadFrom(Path.Combine(_currentPath, "Disco.Ontology.dll"));
            _assemblyVersion = ontologyAssembly.GetName().Version.ToString();

            if (!File.Exists(assemblyName))
            {
                return null;
            }

            Assembly dynamicAssembly = Assembly.LoadFrom(assemblyName);
            if (null != dynamicAssembly)
            {
                var fileVersionDynamicAssembly =
                    FileVersionInfo.GetVersionInfo(dynamicAssembly.Location).FileVersion;

                if (!fileVersionDynamicAssembly.Equals(_assemblyVersion))
                {
                    dynamicAssembly = null;
                    File.Delete(assemblyName);
                    Debug.WriteLine(String.Format("Assembly deleted. Version {0}", fileVersionDynamicAssembly), assemblyName);
                }
            }

            return dynamicAssembly;
        }
    }
}